Este projeto busca realizar análise do problema proposto pelo Data Science Challenge 2019 - ITA.
Os dados podem ser adquiridos no site: https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques/data
O campo do aprendizado de máquina e da ciência de dados tem se mostrado cada vez mais relevante e aplicável em diversas áreas. Entre as áreas de grande interesse encontra-se a precificação de imóveis residenciais, um desafio que envolve a análise de várias características do imóvel e a estimativa do seu valor de mercado. Essa é uma tarefa complexa, devido à grande quantidade de fatores que influenciam o problema. Características como tamanho, localização, qualidade, idade, entre outras, desempenham um papel fundamental na determinação do preço de venda. No entanto, a relação entre essas características e o preço não é linear e muitas vezes não é facilmente mensurável. Além disso, existem fatores externos e tendências de mercado que também impactam o valor dos imóveis. Portanto, é um desafio desenvolver modelos que sejam capazes de estimar com precisão os preços de venda.
Com base nisso, objetivo deste trabalho é explorar o uso de técnicas de aprendizado indutivo, análise exploratória de dados e aprendizado preditivo para construir um modelo de regressão capaz de estimar o preço de venda de imóveis residenciais. Para isso, utilizaremos o conjunto de dados "House Prices - Advanced Regression Techniques", que contém informações detalhadas sobre características dos imóveis, como tamanho, localização, qualidade, idade, entre outras.
A fim de se alcançar o objetivo proposto, inicialmente será realizda uma análise exploratória dos dados, aplicando estatísticas descritivas e visualização multivariada para compreender a distribuição e relação entre as variáveis do conjunto de dados. Em seguida, faremos o pré-processamento dos dados, incluindo limpeza, redução dimensional e transformações, a fim de preparar os dados para a construção do modelo de regressão. Em sequência, serão utilizadas técnicas de aprendizado preditivo para treinar e avaliar diferentes modelos de regressão, utilizando métricas de desempenho adequadas para a tarefa de precificação de imóveis. Por fim, serão avaliadas a capacidade de generalização dos modelos por meio de validação cruzada e análise das métricas de erro.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
from scipy import stats
from sklearn.impute import KNNImputer
import functions
from sklearn.feature_selection import mutual_info_regression
from scipy.stats import chi2_contingency
import os
print(os.getcwd())
c:\Users\Acer\Documents\Cepa\Graduacao\Semestre9\PO233\Projeto\DSChallenge2019\src
train = pd.read_csv('../dataset/train.csv', index_col = 'Id')
test= pd.read_csv('../dataset/test.csv', index_col = 'Id')
train.head()
| MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | LotConfig | ... | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Id | |||||||||||||||||||||
| 1 | 60 | RL | 65.0 | 8450 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2008 | WD | Normal | 208500 |
| 2 | 20 | RL | 80.0 | 9600 | Pave | NaN | Reg | Lvl | AllPub | FR2 | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2007 | WD | Normal | 181500 |
| 3 | 60 | RL | 68.0 | 11250 | Pave | NaN | IR1 | Lvl | AllPub | Inside | ... | 0 | NaN | NaN | NaN | 0 | 9 | 2008 | WD | Normal | 223500 |
| 4 | 70 | RL | 60.0 | 9550 | Pave | NaN | IR1 | Lvl | AllPub | Corner | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2006 | WD | Abnorml | 140000 |
| 5 | 60 | RL | 84.0 | 14260 | Pave | NaN | IR1 | Lvl | AllPub | FR2 | ... | 0 | NaN | NaN | NaN | 0 | 12 | 2008 | WD | Normal | 250000 |
5 rows × 80 columns
# check missing values:
missing_values = pd.DataFrame(data={
'Feature_name': train.columns,
'missing_values': train.isnull().sum(),
'percentage': train.isnull().sum() / len(train) * 100,
'type': train.dtypes
})
missing_values.sort_values(by='percentage', ascending=False).head(20)
| Feature_name | missing_values | percentage | type | |
|---|---|---|---|---|
| PoolQC | PoolQC | 1453 | 99.520548 | object |
| MiscFeature | MiscFeature | 1406 | 96.301370 | object |
| Alley | Alley | 1369 | 93.767123 | object |
| Fence | Fence | 1179 | 80.753425 | object |
| MasVnrType | MasVnrType | 872 | 59.726027 | object |
| FireplaceQu | FireplaceQu | 690 | 47.260274 | object |
| LotFrontage | LotFrontage | 259 | 17.739726 | float64 |
| GarageYrBlt | GarageYrBlt | 81 | 5.547945 | float64 |
| GarageCond | GarageCond | 81 | 5.547945 | object |
| GarageType | GarageType | 81 | 5.547945 | object |
| GarageFinish | GarageFinish | 81 | 5.547945 | object |
| GarageQual | GarageQual | 81 | 5.547945 | object |
| BsmtExposure | BsmtExposure | 38 | 2.602740 | object |
| BsmtFinType2 | BsmtFinType2 | 38 | 2.602740 | object |
| BsmtCond | BsmtCond | 37 | 2.534247 | object |
| BsmtQual | BsmtQual | 37 | 2.534247 | object |
| BsmtFinType1 | BsmtFinType1 | 37 | 2.534247 | object |
| MasVnrArea | MasVnrArea | 8 | 0.547945 | float64 |
| Electrical | Electrical | 1 | 0.068493 | object |
| MSSubClass | MSSubClass | 0 | 0.000000 | int64 |
#remover colunas com mais de 50% de valores faltantes:
features_to_drop = missing_values[missing_values['percentage'] > 50]['Feature_name'].values
train = train.drop(features_to_drop, axis='columns')
test = test.drop(features_to_drop, axis='columns')
train
| MSSubClass | MSZoning | LotFrontage | LotArea | Street | LotShape | LandContour | Utilities | LotConfig | LandSlope | ... | EnclosedPorch | 3SsnPorch | ScreenPorch | PoolArea | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Id | |||||||||||||||||||||
| 1 | 60 | RL | 65.0 | 8450 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 2 | 2008 | WD | Normal | 208500 |
| 2 | 20 | RL | 80.0 | 9600 | Pave | Reg | Lvl | AllPub | FR2 | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 5 | 2007 | WD | Normal | 181500 |
| 3 | 60 | RL | 68.0 | 11250 | Pave | IR1 | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 9 | 2008 | WD | Normal | 223500 |
| 4 | 70 | RL | 60.0 | 9550 | Pave | IR1 | Lvl | AllPub | Corner | Gtl | ... | 272 | 0 | 0 | 0 | 0 | 2 | 2006 | WD | Abnorml | 140000 |
| 5 | 60 | RL | 84.0 | 14260 | Pave | IR1 | Lvl | AllPub | FR2 | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 12 | 2008 | WD | Normal | 250000 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1456 | 60 | RL | 62.0 | 7917 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 8 | 2007 | WD | Normal | 175000 |
| 1457 | 20 | RL | 85.0 | 13175 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 2 | 2010 | WD | Normal | 210000 |
| 1458 | 70 | RL | 66.0 | 9042 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 2500 | 5 | 2010 | WD | Normal | 266500 |
| 1459 | 20 | RL | 68.0 | 9717 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 112 | 0 | 0 | 0 | 0 | 4 | 2010 | WD | Normal | 142125 |
| 1460 | 20 | RL | 75.0 | 9937 | Pave | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | 0 | 0 | 0 | 0 | 6 | 2008 | WD | Normal | 147500 |
1460 rows × 75 columns
# contagem de valores únicos:
unique_values = pd.DataFrame(data={"Feature_name": train.columns, "unique_values": train.nunique()})
unique_values.sort_values(by='unique_values', ascending=True).head(20)
| Feature_name | unique_values | |
|---|---|---|
| Street | Street | 2 |
| Utilities | Utilities | 2 |
| CentralAir | CentralAir | 2 |
| LandSlope | LandSlope | 3 |
| BsmtHalfBath | BsmtHalfBath | 3 |
| GarageFinish | GarageFinish | 3 |
| PavedDrive | PavedDrive | 3 |
| HalfBath | HalfBath | 3 |
| BsmtCond | BsmtCond | 4 |
| BsmtExposure | BsmtExposure | 4 |
| Fireplaces | Fireplaces | 4 |
| ExterQual | ExterQual | 4 |
| KitchenAbvGr | KitchenAbvGr | 4 |
| LandContour | LandContour | 4 |
| LotShape | LotShape | 4 |
| BsmtFullBath | BsmtFullBath | 4 |
| FullBath | FullBath | 4 |
| KitchenQual | KitchenQual | 4 |
| BsmtQual | BsmtQual | 4 |
| ExterCond | ExterCond | 5 |
Não temos nenhum atributo com valores únicos que possa ser descartado a priori
A análise exploratória de dados é um processo inicial na análise de dados, no qual o objetivo é obter uma compreensão básica dos dados e identificar padrões e tendências. É uma etapa crucial antes de aplicar técnicas estatísticas mais avançadas ou construir modelos preditivos.
# Determina os atributos numéricos
numerical_features = train.select_dtypes(include=['int64', 'float64']).columns
# Determina os atributos quantitativos discretos e continuos
discreet_quantitative = []
continuous_quantitative = []
for column in train[numerical_features].columns:
unique_values = train[numerical_features][column].nunique()
if unique_values <= 16:
discreet_quantitative.append(column)
else:
continuous_quantitative.append(column)
# Exibe as colunas selecionadas
print(discreet_quantitative)
print(continuous_quantitative)
['MSSubClass', 'OverallQual', 'OverallCond', 'BsmtFullBath', 'BsmtHalfBath', 'FullBath', 'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd', 'Fireplaces', 'GarageCars', 'PoolArea', 'MoSold', 'YrSold'] ['LotFrontage', 'LotArea', 'YearBuilt', 'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', '1stFlrSF', '2ndFlrSF', 'LowQualFinSF', 'GrLivArea', 'GarageYrBlt', 'GarageArea', 'WoodDeckSF', 'OpenPorchSF', 'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'MiscVal', 'SalePrice']
Calculam-se medidas estatísticas como média, mediana, moda, desvio padrão, mínimo, máximo e quartis. Essas medidas fornecem uma visão geral das características centrais, dispersão e distribuição dos dados.
# Calcula o resumo estatístico usando a função describe()
summary = train[continuous_quantitative].describe().transpose()
# Adiciona a moda como uma nova coluna no resumo estatístico
summary['moda'] = train[continuous_quantitative].mode().transpose()[0]
# Define os nomes das colunas da tabela
colunas = ['Atributo', 'Média', 'Mediana', 'Moda', 'Desvio Padrão', 'Mínimo', '25%', '50%', '75%', 'Máximo']
# Cria a matriz com o resumo estatístico
matriz_resumo = pd.DataFrame(columns=colunas)
matriz_resumo['Atributo'] = summary.index
matriz_resumo[['Média', 'Mediana', 'Moda', 'Desvio Padrão', 'Mínimo', '25%', '50%', '75%', 'Máximo']] = summary[['mean', '50%', 'moda', 'std', 'min', '25%', '50%', '75%', 'max']].values
# Imprime a tabela
print(matriz_resumo)
Atributo Média Mediana Moda Desvio Padrão Mínimo
0 LotFrontage 70.049958 69.0 60.0 24.284752 21.0 \
1 LotArea 10516.828082 9478.5 7200.0 9981.264932 1300.0
2 YearBuilt 1971.267808 1973.0 2006.0 30.202904 1872.0
3 YearRemodAdd 1984.865753 1994.0 1950.0 20.645407 1950.0
4 MasVnrArea 103.685262 0.0 0.0 181.066207 0.0
5 BsmtFinSF1 443.639726 383.5 0.0 456.098091 0.0
6 BsmtFinSF2 46.549315 0.0 0.0 161.319273 0.0
7 BsmtUnfSF 567.240411 477.5 0.0 441.866955 0.0
8 TotalBsmtSF 1057.429452 991.5 0.0 438.705324 0.0
9 1stFlrSF 1162.626712 1087.0 864.0 386.587738 334.0
10 2ndFlrSF 346.992466 0.0 0.0 436.528436 0.0
11 LowQualFinSF 5.844521 0.0 0.0 48.623081 0.0
12 GrLivArea 1515.463699 1464.0 864.0 525.480383 334.0
13 GarageYrBlt 1978.506164 1980.0 2005.0 24.689725 1900.0
14 GarageArea 472.980137 480.0 0.0 213.804841 0.0
15 WoodDeckSF 94.244521 0.0 0.0 125.338794 0.0
16 OpenPorchSF 46.660274 25.0 0.0 66.256028 0.0
17 EnclosedPorch 21.954110 0.0 0.0 61.119149 0.0
18 3SsnPorch 3.409589 0.0 0.0 29.317331 0.0
19 ScreenPorch 15.060959 0.0 0.0 55.757415 0.0
20 MiscVal 43.489041 0.0 0.0 496.123024 0.0
21 SalePrice 180921.195890 163000.0 140000.0 79442.502883 34900.0
25% 50% 75% Máximo
0 59.00 69.0 80.00 313.0
1 7553.50 9478.5 11601.50 215245.0
2 1954.00 1973.0 2000.00 2010.0
3 1967.00 1994.0 2004.00 2010.0
4 0.00 0.0 166.00 1600.0
5 0.00 383.5 712.25 5644.0
6 0.00 0.0 0.00 1474.0
7 223.00 477.5 808.00 2336.0
8 795.75 991.5 1298.25 6110.0
9 882.00 1087.0 1391.25 4692.0
10 0.00 0.0 728.00 2065.0
11 0.00 0.0 0.00 572.0
12 1129.50 1464.0 1776.75 5642.0
13 1961.00 1980.0 2002.00 2010.0
14 334.50 480.0 576.00 1418.0
15 0.00 0.0 168.00 857.0
16 0.00 25.0 68.00 547.0
17 0.00 0.0 0.00 552.0
18 0.00 0.0 0.00 508.0
19 0.00 0.0 0.00 480.0
20 0.00 0.0 0.00 15500.0
21 129975.00 163000.0 214000.00 755000.0
Utilizam-se gráficos e visualizações adequados para representar os dados quantitativos. Isso pode incluir histogramas, box plots, gráficos de dispersão, gráficos de linha ou gráficos de séries temporais, dependendo da natureza dos dados e do objetivo da análise.
# Configurar o estilo do Seaborn
sns.set()
# Definir o número de subplots por linha e o tamanho da figura
subplots_per_row = 3
figsize = (15, 15)
# Obter todas as colunas de atributos do dataframe train (exceto a última coluna)
attribute_columns = train[continuous_quantitative].columns[:-1]
# Calcular o número total de subplots
num_subplots = len(attribute_columns)
# Calcular o número de linhas
num_rows = (num_subplots - 1) // subplots_per_row + 1
# Criar a figura e os subplots
fig, axes = plt.subplots(num_rows, subplots_per_row, figsize=figsize)
# Converter a matriz de subplots em um array unidimensional
axes = axes.flatten()
# Iterar sobre os atributos e criar os gráficos de dispersão
for i, attribute in enumerate(attribute_columns):
# Selecionar o subplot atual
ax = axes[i]
# Criar o gráfico de dispersão
sns.scatterplot(x=attribute, y=train.columns[-1], data=train, ax=ax)
# Definir o título do gráfico
ax.set_title(f'{attribute} vs SalePrice')
# Definir o nome do eixo y como o nome do atributo alvo
ax.set_ylabel(train[continuous_quantitative].columns[-1])
# Remover os subplots vazios, se houverem
if num_subplots < len(axes):
for j in range(num_subplots, len(axes)):
fig.delaxes(axes[j])
# Ajustar o espaçamento entre os subplots
fig.tight_layout()
# Exibir o gráfico
plt.show()
# Configurar o estilo do Seaborn
sns.set()
# Definir o número de subplots por linha e o tamanho da figura
subplots_per_row = 3
figsize = (15, 15)
# Obter todas as colunas de atributos do dataframe train (exceto a última coluna)
attribute_columns = train[continuous_quantitative].columns[:-1]
# Calcular o número total de subplots
num_subplots = len(attribute_columns)
# Calcular o número de linhas
num_rows = (num_subplots - 1) // subplots_per_row + 1
# Criar a figura e os subplots
fig, axes = plt.subplots(num_rows, subplots_per_row, figsize=figsize)
# Converter a matriz de subplots em um array unidimensional
axes = axes.flatten()
# Iterar sobre os atributos e criar os histogramas com as curvas de distribuição
for i, attribute in enumerate(attribute_columns):
# Selecionar o subplot atual
ax = axes[i]
# Criar o histograma com a curva de distribuição
sns.histplot(data=train, x=attribute, kde=True, ax=ax)
# Definir o título do subplot
ax.set_title(attribute)
# Remover os subplots vazios, se houverem
if num_subplots < len(axes):
for j in range(num_subplots, len(axes)):
fig.delaxes(axes[j])
# Ajustar o espaçamento entre os subplots
fig.tight_layout()
# Exibir o gráfico
plt.show()
Explora-se a relação entre diferentes variáveis quantitativas por meio de medidas de correlação, como o coeficiente de correlação de Pearson. Isso ajuda a identificar a força e a direção da relação entre as variáveis.
corr_matrix = train[continuous_quantitative].corr()
plt.subplots(figsize=(12,9))
plt.title('Matriz de correlação entre as variáveis numéricas.')
sns.heatmap(corr_matrix, vmax=0.9, square=True)
<Axes: title={'center': 'Matriz de correlação entre as variáveis numéricas.'}>
Vamos ver quais variáveis tem correlação alta com o nosso alvo: SalesPrice:
# correlação entre as variáveis numéricas e o preço:
corr_matrix['SalePrice'].sort_values(ascending=False).head(12)
SalePrice 1.000000 GrLivArea 0.708624 GarageArea 0.623431 TotalBsmtSF 0.613581 1stFlrSF 0.605852 YearBuilt 0.522897 YearRemodAdd 0.507101 GarageYrBlt 0.486362 MasVnrArea 0.477493 BsmtFinSF1 0.386420 LotFrontage 0.351799 WoodDeckSF 0.324413 Name: SalePrice, dtype: float64
Intuitivamente falando, é provavel que as variáveis mais correlacionadas tenham maior poder preditivo sobre o preço. Entretando devemos verificar se há correlação entre elas mesmas, para fins de simplificação, vamos pegar os atributos cuja correlação seja maior que $0.5$
#atributos com correlação maior que 0.5:
features_correlated = corr_matrix['SalePrice'].sort_values(ascending=False).loc[lambda x : x > 0.5].index
#plotar a correlação entre as variáveis com correlação maior que 0.5:
corr_matrix = train[features_correlated].corr()
plt.subplots(figsize=(12,9))
plt.title('Matriz de correlação entre as variáveis numéricas com correlação maior que 0.5.')
sns.heatmap(corr_matrix, vmax=0.9, square=True)
<Axes: title={'center': 'Matriz de correlação entre as variáveis numéricas com correlação maior que 0.5.'}>
Observamos que algumas variáveis são fortemente correlacionadas:
GarageArea e GarageCars : Faz sentido se pensar que o aumento do numero de carros na garagem exige uma garagem maior.
TotalBsmtSF e 1stFlrSF: A relação entre o total de metragem do imovel com a metragem do primeiro andar também é pertinente.
TotRmsAbvGrd e GrLivArea: O número total de salas em relação ao tamanho da sala de estar também é pertinente
Portanto vamos deletar 1stFlrSF, GarageArea e GrLivArea (Escolhidos de forma arbitrária).
# remover as variáveis com correlação maior que 0.5:
features_correlated = features_correlated.drop(['GarageArea', '1stFlrSF', 'GrLivArea'])
train = train.drop(['GarageArea', '1stFlrSF', 'GrLivArea'], axis='columns')
test = test.drop(['GarageArea', '1stFlrSF', 'GrLivArea'], axis='columns')
# Determina novamente os atributos numéricos após a exclusão de dados
numerical_features = train.select_dtypes(include=['int64', 'float64']).columns
# Determina os atributos quantitativos discretos e continuos
discreet_quantitative = []
continuous_quantitative = []
for column in train[numerical_features].columns:
unique_values = train[numerical_features][column].nunique()
if unique_values <= 16:
discreet_quantitative.append(column)
else:
continuous_quantitative.append(column)
# Exibe as colunas selecionadas
print(discreet_quantitative)
print(continuous_quantitative)
['MSSubClass', 'OverallQual', 'OverallCond', 'BsmtFullBath', 'BsmtHalfBath', 'FullBath', 'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd', 'Fireplaces', 'GarageCars', 'PoolArea', 'MoSold', 'YrSold'] ['LotFrontage', 'LotArea', 'YearBuilt', 'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', '2ndFlrSF', 'LowQualFinSF', 'GarageYrBlt', 'WoodDeckSF', 'OpenPorchSF', 'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'MiscVal', 'SalePrice']
Vamos verificar se há valores outliers nos atributos e vamos realizar o tratamento deles. Inicialmente iremos fazer uma inspeção visual por meio do plot de boxplots
# Configurar o estilo do Seaborn
sns.set()
# Configurar o layout dos subplots
n = 4 # Número de subplots por linha
num_features = len(continuous_quantitative)
num_subplots = num_features // n + (num_features % n > 0)
fig, axes = plt.subplots(num_subplots, 4, figsize=(15, num_subplots * 5))
# Iterar sobre as features numéricas (exceto a coluna do atributo alvo)
for i, feature in enumerate(continuous_quantitative):
ax = axes[i // n, i % n] # Acessar o subplot correspondente
sns.boxplot(y=train[feature], ax=ax, orient='v') # Plotar boxplot na vertical
ax.set_title(feature) # Definir o título do subplot
# Remover subplots vazios, se necessário
if num_features % n != 0:
for i in range(num_features % n, n):
fig.delaxes(axes[-1, i])
# Ajustar o espaçamento entre os subplots
fig.tight_layout()
# Exibir o gráfico
plt.show()
O z-score nos da uma idéia do quanto um determinado ponto está afastado da média dos dados, isto é , ele mede quantos desvios padrão abaixo ou acima da média populacional ou amostral os dados estão: $$ z=\frac{x-\mu}{\sigma} $$ Onde:
Assumindo uma distribuição normal, sabe-se que 99,7% dos dados estão à uma distância de três desvios padrão da média. Com base nisso, será considerado nesse trabalho que dados com distância acima de três desvios padrão serão considerados outliers.
# Filtrar apenas as colunas numéricas do DataFrame train após o drop
numerical_features = train.select_dtypes(include=['int64', 'float64']).columns
# Criar uma cópia do DataFrame train
train_ZS = train[continuous_quantitative]
# Calcular e atribuir o Z-score para cada coluna numérica, exceto a última
for col in continuous_quantitative[:-1]:
col_values = train_ZS[col].values
zscore = stats.zscore(col_values) # Calcula o Z-score para todos os valores da coluna
outliers = (zscore > 3) | (zscore < -3)
train_ZS.loc[outliers, col] = np.nan
# Criação do imputador KNN
imputer = KNNImputer(n_neighbors=15, weights='uniform', metric='nan_euclidean')
# Ajuste do imputador aos dados
imputer.fit(train_ZS)
# Imputação dos valores ausentes
train_ZS = pd.DataFrame(imputer.transform(train_ZS), columns=train_ZS.columns)
Percentil:
Amplitude interquartil:
É Diferença entre o terceiro quartil (Q3) e o primeiro quartil (Q1) Para identificar Outlier rom amplitude interquartil serão realizados os procedimentos abaixo:
# Criar novo DataFrame com as colunas numéricas
train_IQR = train[continuous_quantitative]
# Calcular o IQR e identificar outliers
for col in continuous_quantitative[:-1]:
Q1 = train_IQR[col].quantile(0.25)
Q3 = train_IQR[col].quantile(0.75)
IQR = Q3 - Q1
limIn = Q1 - (IQR * 1.5)
limSp = Q3 + (IQR * 1.5)
# Substituir outliers por np.nan
train_IQR.loc[(train_IQR[col] < limIn) | (train_IQR[col] > limSp), col] = np.nan
# Criar imputador KNN
imputer = KNNImputer(n_neighbors=15, weights='uniform', metric='nan_euclidean')
# Ajustar imputador aos dados
imputer.fit(train_IQR)
# Imputar valores ausentes
train_IQR = pd.DataFrame(imputer.transform(train_IQR), columns=train_IQR.columns)
Em ambos os métodos acima, foram identificados os outliers e para cada um deles foi atribuido um valor ausente, ou seja, eles foram excluidos dos dados. Para preencher os dados ausentes foi utilizada uma técnica de imputação de valores com base em proximidade, denominada KNN. O imputador KNN (K-Nearest Neighbors) é uma técnica dque pode ser utilizada para preencher valores ausentes em conjuntos de dados. Ele é um algoritmo de aprendizado de máquina capaz de prever valores ausentes com base na similaridade entre as amostras do conjunto de dados.
Inicialmente ele encontra os K vizinhos mais próximos (específicamente 15 nesse trabalho) para cada valor ausente, calculando a distância entre a amostra com valor ausente e todas as outras amostras no conjunto de dados. Ele seleciona as K amostras mais próximas com base em uma métrica de distância que, neste caso, é a distância euclidiana.
Em seguida, o KNN calcula um valor imputado para o valor ausente com base na média arimética dos valores dos vizinhos (vizinhos com pesos iguais). Por fim, o valor imputado é atribuído a cada valor ausente no conjunto de dados.
Inicialmente vamos converter os atributos do tipo 'object' para 'category'
# Selecionar as colunas do tipo 'object'
object_columns = train.select_dtypes(include='object').columns
# Converter as colunas para o tipo 'category'
train[object_columns] = train[object_columns].astype('category')
Após isso, vamos selecionar os dados qualitativos:
qualitative_features = train.columns[~train.columns.isin(train[continuous_quantitative].columns)]
train[qualitative_features]
| MSSubClass | MSZoning | Street | LotShape | LandContour | Utilities | LotConfig | LandSlope | Neighborhood | Condition1 | ... | GarageFinish | GarageCars | GarageQual | GarageCond | PavedDrive | PoolArea | MoSold | YrSold | SaleType | SaleCondition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Id | |||||||||||||||||||||
| 1 | 60 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | CollgCr | Norm | ... | RFn | 2 | TA | TA | Y | 0 | 2 | 2008 | WD | Normal |
| 2 | 20 | RL | Pave | Reg | Lvl | AllPub | FR2 | Gtl | Veenker | Feedr | ... | RFn | 2 | TA | TA | Y | 0 | 5 | 2007 | WD | Normal |
| 3 | 60 | RL | Pave | IR1 | Lvl | AllPub | Inside | Gtl | CollgCr | Norm | ... | RFn | 2 | TA | TA | Y | 0 | 9 | 2008 | WD | Normal |
| 4 | 70 | RL | Pave | IR1 | Lvl | AllPub | Corner | Gtl | Crawfor | Norm | ... | Unf | 3 | TA | TA | Y | 0 | 2 | 2006 | WD | Abnorml |
| 5 | 60 | RL | Pave | IR1 | Lvl | AllPub | FR2 | Gtl | NoRidge | Norm | ... | RFn | 3 | TA | TA | Y | 0 | 12 | 2008 | WD | Normal |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1456 | 60 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | Gilbert | Norm | ... | RFn | 2 | TA | TA | Y | 0 | 8 | 2007 | WD | Normal |
| 1457 | 20 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | NWAmes | Norm | ... | Unf | 2 | TA | TA | Y | 0 | 2 | 2010 | WD | Normal |
| 1458 | 70 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | Crawfor | Norm | ... | RFn | 1 | TA | TA | Y | 0 | 5 | 2010 | WD | Normal |
| 1459 | 20 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | NAmes | Norm | ... | Unf | 1 | TA | TA | Y | 0 | 4 | 2010 | WD | Normal |
| 1460 | 20 | RL | Pave | Reg | Lvl | AllPub | Inside | Gtl | Edwards | Norm | ... | Fin | 1 | TA | TA | Y | 0 | 6 | 2008 | WD | Normal |
1460 rows × 53 columns
Para dados qualitativos, que envolvem atributos ou categorias, a análise exploratória pode envolver os seguintes aspectos:
Calculam-se as frequências absolutas e relativas de cada categoria presente nos dados qualitativos. Isso permite entender a distribuição e proporções de cada categoria.
# Dividindo os atributos em duas metades
half = len(train[qualitative_features]) // 2
first_half = train[qualitative_features][:half]
second_half = train[qualitative_features][half:]
# Configurações do estilo seaborn
sns.set(style='whitegrid')
# Configuração dos subplots para a primeira metade dos atributos
num_first_half = len(first_half.columns)
num_rows_first_half = (num_first_half - 1) // 4 + 1
fig, axes = plt.subplots(nrows=num_rows_first_half, ncols=4, figsize=(15, 4*num_rows_first_half))
# Plotagem dos histogramas para a primeira metade dos atributos
for i, col in enumerate(first_half.columns):
ax = axes[i // 4, i % 4]
sns.histplot(data=train, x=col, ax=ax, stat='count', discrete=True)
ax.set_title(col, fontsize=10)
ax.set_xlabel('')
ax.set_ylabel('')
# Definir as etiquetas do eixo x na vertical
ax.set_xticklabels(ax.get_xticklabels(), rotation='vertical')
# Remover subplots vazios, se houverem
if num_first_half < num_rows_first_half * 4:
for i in range(num_first_half, num_rows_first_half * 4):
fig.delaxes(axes[i // 4, i % 4])
# Ajuste de espaçamento entre subplots
plt.tight_layout()
# Configuração dos subplots para a segunda metade dos atributos
num_second_half = len(second_half.columns)
num_rows_second_half = (num_second_half - 1) // 4 + 1
fig, axes = plt.subplots(nrows=num_rows_second_half, ncols=4, figsize=(15, 4*num_rows_second_half))
# Plotagem dos histogramas para a segunda metade dos atributos
for i, col in enumerate(second_half.columns):
ax = axes[i // 4, i % 4]
sns.histplot(data=train, x=col, ax=ax, stat='count', discrete=True)
ax.set_title(col, fontsize=10)
ax.set_xlabel('')
ax.set_ylabel('')
# Definir as etiquetas do eixo x na vertical
ax.set_xticklabels(ax.get_xticklabels(), rotation='vertical')
# Remover subplots vazios, se houverem
if num_second_half < num_rows_second_half * 4:
for i in range(num_second_half, num_rows_second_half * 4):
fig.delaxes(axes[i // 4, i % 4])
# Ajuste de espaçamento entre subplots
plt.tight_layout()
# Exibição dos plots
plt.show()
A distribuição do atributo alvo em relação aos demais atributos refere-se à relação entre a variável alvo (também conhecida como variável dependente, resposta ou variável a ser prevista) e as outras variáveis do conjunto de dados (também conhecidas como variáveis independentes, preditoras ou explicativas).
Essa análise busca compreender como a distribuição ou os valores da variável alvo variam ou são influenciados pelas diferentes categorias ou níveis das outras variáveis. Em outras palavras, examina-se como a variável alvo se comporta em relação a diferentes valores ou grupos das outras variáveis.
# Converter atributos qualitativos para tipo categórico
for col in qualitative_features:
train[col] = train[col].astype('category')
# Verificar valores ausentes
if train[col].isnull().any():
train[col] = train[col].cat.add_categories(['MISSING'])
train[col] = train[col].fillna('MISSING')
def boxplot(x, y, **kwargs):
sns.boxplot(x=x, y=y)
plt.xticks(rotation=90)
# Derreter o DataFrame
melted = pd.melt(train, id_vars=['SalePrice'], value_vars=qualitative_features)
# Configurar o FacetGrid para os boxplots
g = sns.FacetGrid(melted, col="variable", col_wrap=4, sharex=False, sharey=False, height=5)
# Mapear a função boxplot no FacetGrid
g.map(boxplot, "value", "SalePrice")
# Exibir os plots
plt.show()
A ANOVA é um teste estatístico utilizado para comparar a média de um grupo com a média de outros grupos, permitindo determinar se existem diferenças significativas entre os grupos. No gráfico do ANOVA, a disparidade (disparity) é representada no eixo y em uma escala logarítmica. Valores mais altos indicam uma maior disparidade entre as categorias da variável qualitativa em relação à variável de resposta (SalePrice no caso desse código).
Em termos práticos, uma disparidade alta indica que as categorias da variável qualitativa têm uma influência significativa na variável de resposta e podem ser consideradas importantes para explicar as variações nos valores da variável de resposta. Isso sugere que a variável qualitativa pode ser um bom preditor da variável de resposta.
Por outro lado, uma disparidade baixa indica que as categorias da variável qualitativa têm pouca influência ou não são estatisticamente significativas na explicação das variações na variável de resposta. Isso sugere que a variável qualitativa pode não ser relevante ou informativa para prever a variável de resposta.
anv = pd.DataFrame()
anv['feature'] = train[qualitative_features].columns
pvals = []
for c in train[qualitative_features].columns:
samples = []
for cls in train[train[qualitative_features][c].notnull()][c].unique():
s = train[train[qualitative_features][c] == cls]['SalePrice'].values
samples.append(s)
pval = stats.f_oneway(*samples)[1]
pvals.append(pval)
anv['pval'] = pvals
anv['disparity'] = np.log(1. / np.maximum(anv['pval'].values, 1e-100))
anv = anv.sort_values('disparity', ascending=False)
plt.figure(figsize=(15, 6))
sns.barplot(data=anv, x='feature', y='disparity')
plt.xticks(rotation=90)
plt.xlabel('Feature')
plt.ylabel('Disparity (log-scale)')
plt.title('ANOVA - Disparity of Qualitative Features')
plt.show()
O teste qui-quadrado, também conhecido como teste de qui-quadrado de Pearson, é um teste estatístico utilizado para determinar se existe uma associação significativa entre duas variáveis categóricas. Ele baseia-se na comparação entre as frequências observadas e as frequências esperadas sob a hipótese nula de que não há associação entre as variáveis. A ideia é verificar se as diferenças observadas entre as frequências são grandes o suficiente para serem consideradas estatisticamente significativas.
O valor de p-value é uma medida estatística que indica a evidência contra a hipótese nula em um teste estatístico. Em geral, ele é usado para avaliar se existe uma associação significativa entre duas variáveis em um teste de independência, como o teste do qui-quadrado.
Um p-value baixo (geralmente menor que 0,05 ou 0,01) indica que há evidências estatísticas significativas para rejeitar a hipótese nula. Isso sugere que existe uma associação ou diferença estatisticamente significativa entre as variáveis testadas.
Um p-value alto (geralmente maior que 0,05 ou 0,01) indica que não há evidências suficientes para rejeitar a hipótese nula. Isso sugere que não há uma associação ou diferença estatisticamente significativa entre as variáveis testadas.
# Realizar o teste qui-quadrado e obter os resultados
results = []
for column in train[qualitative_features].columns:
contingency_table = pd.crosstab(train[column], train['SalePrice'])
chi2, p_value, _, _ = chi2_contingency(contingency_table)
results.append((column, chi2, p_value))
# Criar um DataFrame com os resultados
results_df = pd.DataFrame(results, columns=['Feature', 'Chi2', 'P-value'])
# Ordenar os resultados pelo valor de p-value (do menor para o maior)
results_df.sort_values(by='P-value', inplace=True)
# Plotar o gráfico de barras dos valores de p-value
plt.figure(figsize=(12, 6))
sns.barplot(data=results_df, x='Feature', y='P-value')
plt.xticks(rotation=90)
plt.xlabel('Feature')
plt.ylabel('P-value')
plt.title('Teste Qui-quadrado: Valores de P-value por Feature')
plt.tight_layout()
plt.show()
as variáveis para o modelo são as seguintes:
features_correlated
Index(['SalePrice', 'TotalBsmtSF', 'YearBuilt', 'YearRemodAdd'], dtype='object')
#atributos com valores faltantes:
missing_values.loc[features_correlated]['missing_values']
SalePrice 0 OverallQual 0 GarageCars 0 TotalBsmtSF 0 FullBath 0 TotRmsAbvGrd 0 YearBuilt 0 YearRemodAdd 0 Name: missing_values, dtype: int64
Não há valores faltantes.
Nessa seção vamos realizar a seleção de atributos relevantes para o modelo.
Nessa seção vamos treinar um modelo de regressão que gere uma predição dos valores de teste
from sklearn.model_selection import train_test_split
# regressao linear
from sklearn.linear_model import LinearRegression
#arvore de deciao
from sklearn.tree import DecisionTreeRegressor
# SVM
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
from sklearn.ensemble import RandomForestRegressor
# Adaline
from sklearn.linear_model import SGDRegressor
#MLP
from sklearn.neural_network import MLPClassifier, MLPRegressor
# Naive Bayes
from sklearn.naive_bayes import MultinomialNB, GaussianNB
#cross validation
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import cross_validate
new_train = train[features_correlated]
X = new_train.drop(['SalePrice'], axis=1).copy()
Y = new_train['SalePrice'] # here we need to remove unnacessary columns if exist
train_X, test_X, train_Y, test_Y = train_test_split(X, Y, test_size=0.3, random_state=1)
new_train
| SalePrice | OverallQual | GarageCars | TotalBsmtSF | FullBath | TotRmsAbvGrd | YearBuilt | YearRemodAdd | |
|---|---|---|---|---|---|---|---|---|
| Id | ||||||||
| 1 | 208500 | 7 | 2 | 856 | 2 | 8 | 2003 | 2003 |
| 2 | 181500 | 6 | 2 | 1262 | 2 | 6 | 1976 | 1976 |
| 3 | 223500 | 7 | 2 | 920 | 2 | 6 | 2001 | 2002 |
| 4 | 140000 | 7 | 3 | 756 | 1 | 7 | 1915 | 1970 |
| 5 | 250000 | 8 | 3 | 1145 | 2 | 9 | 2000 | 2000 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1456 | 175000 | 6 | 2 | 953 | 2 | 7 | 1999 | 2000 |
| 1457 | 210000 | 6 | 2 | 1542 | 2 | 7 | 1978 | 1988 |
| 1458 | 266500 | 7 | 1 | 1152 | 2 | 9 | 1941 | 2006 |
| 1459 | 142125 | 5 | 1 | 1078 | 1 | 5 | 1950 | 1996 |
| 1460 | 147500 | 5 | 1 | 1256 | 1 | 6 | 1965 | 1965 |
1460 rows × 8 columns
Separamos o dataset de treino na proporção de 70-30 em um dataset de treino e outro de teste. Perceba que o tamanho do dataset de teste realmente é 30% do dataset original.
print(train_X.shape, train_Y.shape)
print(test_X.shape, len(test_Y))
(1022, 7) (1022,) (438, 7) 438
O primeiro modelo será uma regressão linear utilizando a biblioteca do scikit-learn de linear_model importando a classe LinearRegression
LR = LinearRegression()
LR.fit(train_X, train_Y)
LR_predicted = LR.predict(test_X)
scoring={'R_squared':'r2','MSE':'neg_mean_squared_error'}
def CrossVal(estimator):
scores = cross_validate(estimator, X, y, cv=10, scoring=scoring)
r2 = scores['test_R_squared'].mean()
mse = abs(scores['test_Square Root of MSE'].mean())
print('R_squared:', r2)
print('Square Root of MSE:', np.sqrt(mse))
CrossVal(LR)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[35], line 12 10 print('R_squared:', r2) 11 print('Square Root of MSE:', np.sqrt(mse)) ---> 12 CrossVal(LR) Cell In[35], line 7, in CrossVal(estimator) 6 def CrossVal(estimator): ----> 7 scores = cross_validate(estimator, X, y, cv=10, scoring=scoring) 8 r2 = scores['test_R_squared'].mean() 9 mse = abs(scores['test_Square Root of MSE'].mean()) NameError: name 'cross_validate' is not defined
O segundo modelo será um regressor de Árvore de Decisão utilizando a biblioteca do scikit-learn de tree importando a classe DecisionTreeRegressor
DTR = DecisionTreeRegressor(criterion='squared_error', max_depth=15, min_samples_split=5, min_samples_leaf=5)
DTR.fit(train_X, train_Y)
DTR_predicted = DTR.predict(test_X)
O terceiro modelo será um SVM utilizando a biblioteca do scikit-learn de svm importando a classe SVR
SVM = make_pipeline(RobustScaler(), RandomForestRegressor())
SVM.fit(train_X, train_Y)
SVM_predicted = SVM.predict(test_X)
ADALINE = SGDRegressor(loss='huber', learning_rate='constant', eta0=0.01, max_iter=1000)
ADALINE.fit(train_X, train_Y)
ADALINE_predicted = ADALINE.predict(test_X)
MLP = MLPClassifier(hidden_layer_sizes=(40,), activation='logistic', solver='adam', max_iter=1000, early_stopping=True, validation_fraction=0.1)
MLP.fit(train_X, train_Y)
MLP_predicted = MLP.predict(test_X)
NB = GaussianNB(var_smoothing=1e-9)
NB.fit(train_X, train_Y)
NB_predicted = NB.predict(test_X)
proba = NB.predict_proba(test_X)
Nessa seção vamos avaliar a performance do modelo gerado.
Os principais critério que vamos avaliar vão ser os seguintes:
O primeiro é o coeficiente de determinação, usualmente expresso por $R^2$. O coeficiente de determinação é a razão da variância do alvo, explicado ou predito pelo modelo, pela a variância total do alvo. É um valor com limites entre $0$ e $1$, e quanto mais próximo de 1 maior a capacidade do modelo em explicar ou prever a variância da variável alvo. $$R^2 = \frac{\sum(\hat{y} - \bar{y})^2}{\sum(y - \bar{y})^2}$$
A segunda métrica é o erro médio quadrático (Mean Squared Error). O $MSE$ é a média das diferenças entre o valor alvo predito e o valor real ao quadrado. Nesse sentido, é sempre maior que zero, e quanto menor o valor de $MSE$ maior a acurácia das predições do modelo. Nesse projeto, iremos utilizar a raiz quadrada de $MSE$, chamada de $RMSE$. $$RMSE = \sqrt{\frac{1}{n}\sum^n_{i=1}(y_i-p_i)^2}$$
# avaliando
from sklearn.metrics import mean_squared_error, r2_score
from functions.actual_vs_pred_plot import actual_vs_pred_plot
from functions.model_residual_plot import model_residual_plot
from functions.regression_metrics import regression_metrics
from functions.model_dist_plot import model_dist_plot
import seaborn as sns
list_models_name = [
"Regressão Linear",
"Árvore de Decisão",
"SVM",
"ADALINE",
"MLP",
"Naive Bayes",
]
list_models = [LR, DTR, SVM, ADALINE, MLP, NB]
list_models_predicted = [LR_predicted, DTR_predicted, SVM_predicted, ADALINE_predicted, MLP_predicted, NB_predicted]
list_models_RMSE = []
list_models_R2 = []
for i in range(0, len(list_models)):
model_predicted = list_models_predicted[i]
model_name = list_models_name[i]
print("\nModelo " + model_name + ":")
rmse, r2 = regression_metrics(model_predicted, test_Y,model_name)
list_models_RMSE.append(rmse)
list_models_R2.append(r2)
actual_vs_pred_plot(test_Y, model_predicted,model_name)
# Create residual plot
model_residual_plot(test_Y, model_predicted, model_name)
model_dist_plot(test_Y, model_predicted, model_name)
Modelo Regressão Linear: Regressão Linear RMSE: 40172.0293046117 Regressão Linear R2: 0.7739883057444342
<Figure size 1200x1200 with 0 Axes>
Modelo Árvore de Decisão: Árvore de Decisão RMSE: 37643.909164987715 Árvore de Decisão R2: 0.8015400841653715
<Figure size 1200x1200 with 0 Axes>
Modelo SVM: SVM RMSE: 30074.595391558858 SVM R2: 0.8733273209234674
<Figure size 1200x1200 with 0 Axes>
Modelo ADALINE: ADALINE RMSE: 65132.019414231516 ADALINE R2: 0.40588234724384553
<Figure size 1200x1200 with 0 Axes>
Modelo MLP: MLP RMSE: 117693.42069767769 MLP R2: -0.9399375095874665
<Figure size 1200x1200 with 0 Axes>
Modelo Naive Bayes: Naive Bayes RMSE: 48546.441365729945 Naive Bayes R2: 0.6699359918192762
<Figure size 1200x1200 with 0 Axes>
Comparando os $RMSE$ de cada modelo gerado.
# comparando os modelos por RMSE:
sns.set_theme(style="whitegrid")
plt.figure(figsize=(10, 5))
ax = sns.barplot(x=list_models_name, y=list_models_RMSE)
Portanto, seguindo a métrica de Root Mean Squared Error como a principal para fazer seleção dos modelos, o melhor modelo foi:
model_index = np.argmin(list_models_RMSE)
model_name = list_models_name[model_index]
model_predicted = list_models_predicted[model_index]
print("\nModelo com melhor R2: " + model_name)
print("RMSE: " + str(list_models_RMSE[model_index]))
print("R2: " + str(list_models_R2[model_index]))
Modelo com melhor R2: SVM RMSE: 30074.595391558858 R2: 0.8733273209234674
from sklearn.impute import SimpleImputer
melhor_modelo = SVM
validate_columns = features_correlated.drop(['SalePrice'])
validate_df = test[validate_columns]
# Eliminate these lines after - Preprocess the data to handle missing values
imputer = SimpleImputer(strategy='mean')
validate_df = pd.DataFrame(imputer.fit_transform(validate_df), columns=validate_columns)
validate = validate_df.copy()
validate['SalePrice'] = melhor_modelo.predict(validate_df)
validate
predicted_sale_price = validate
predicted_sale_price.to_csv("SalePrice_predicted.csv", index=False)
predicted_sale_price.head()
| OverallQual | GarageCars | TotalBsmtSF | FullBath | TotRmsAbvGrd | YearBuilt | YearRemodAdd | SalePrice | |
|---|---|---|---|---|---|---|---|---|
| 0 | 5.0 | 1.0 | 882.0 | 1.0 | 5.0 | 1961.0 | 1961.0 | 111703.493333 |
| 1 | 6.0 | 1.0 | 1329.0 | 1.0 | 6.0 | 1958.0 | 1958.0 | 145212.333333 |
| 2 | 5.0 | 2.0 | 928.0 | 2.0 | 6.0 | 1997.0 | 1998.0 | 169701.600000 |
| 3 | 6.0 | 2.0 | 926.0 | 2.0 | 7.0 | 1998.0 | 1998.0 | 194384.000000 |
| 4 | 8.0 | 2.0 | 1280.0 | 2.0 | 5.0 | 1992.0 | 1992.0 | 204112.060000 |